package com.zf.openfeign.caller.config;

import feign.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.openfeign.FeignClientFactory;
import org.springframework.cloud.openfeign.FeignClientFactoryBean;
import org.springframework.cloud.openfeign.Targeter;
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.DispatcherServlet;

import java.util.Enumeration;
import java.util.Objects;


/**
 * <pre>
 * feign官方文档：
 * 1、https://spring.io/projects/spring-cloud-openfeign#learn
 * 2、https://docs.spring.io/spring-cloud-openfeign/reference/4.1/spring-cloud-openfeign.html
 *
 * 该类用来解决feign调用的一些问题
 * </pre>
 */

/**
 * feign初始化，及执行流程：
 * <pre>
 * 1、通过 {@link org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients(AnnotationMetadata, BeanDefinitionRegistry)} 扫描所有的  @FeignClient 注解的接口;
 * 2、给每个接口创建一个 {@link FeignClientFactoryBean} 对象，将注解接口以及  @FeignClient 注解中的信息设置到对象中
 *    然后将 FeignClientFactoryBean 对象注册到容器中;
 * 3、在其他对象中注入 @FeignClient注解的接口时，会 调用对应的 FeignClientFactoryBean 对象的 {@link FeignClientFactoryBean#getTarget} 对象获取代理类，方法执行如下操作：
 *   3.1、处理一下url，根据@FeignClient注解是否填写了url属性来判断是使用 “负载均衡的 {@link Client}，如{@link FeignBlockingLoadBalancerClient} 还是普通的  {@link Client}，如 {@link Client.Default}”,
 *        这个 Client对象 会存放到一个 {@link Feign.Builder} 对象中;
 *   3.2、将@FeignClient注解接口的 类型、name、url 封装成 {@link Target.HardCodedTarget} 类型对象;
 *   3.3、然后使用 {@link Targeter#target(FeignClientFactoryBean, Feign.Builder, FeignClientFactory, Target.HardCodedTarget)} 方法创建代理对象，方法执行如下操作:
 *      3.3.1、其中的 Feign.Builder 参数是创建代理对象的关键，{@link Feign.Builder#target(Target)}方法内部
 *             通过使用 {@link ReflectiveFeign#newInstance(Target)} 创建了代理对象，使用的拦截器类是 {@link SynchronousMethodHandler}
 *
 * 4、以后每次feign接口的方法时，都会进入 {@link SynchronousMethodHandler#invoke(Object[])} 方法，其内才是真正的调用：
 *   4.1、创建一个 {@link RequestTemplate} 对象，<b>也就是在这里，我们会丢失掉原请求的请求头信息</b>
 *   4.2、然后执行 {@link SynchronousMethodHandler#executeAndDecode(RequestTemplate, Request.Options)}
 *   4.3、executeAndDecode() 方法内部会调用 {@link SynchronousMethodHandler#targetRequest(RequestTemplate)} 方法创建一个 {@link Request} 对象：
 *      4.3.1、执行容器中的所有 {@link RequestInterceptor#apply(RequestTemplate)} 方法，没有顺序，都会执行，
 *                     <b>也就是在这里，我们可以自定义 RequestInterceptor 对象，手动将丢失的请求头设置到 RequestTemplate 对象中，或者添加移除一些请求头 </b>
 *      4.3.2、执行 {@link Target.HardCodedTarget#apply(RequestTemplate)} 方法创建出 {@link Request} 对象
 *   4.4、使用 {@link Client} 执行  {@link Request} 对象，发送请求，并解析响应
 *
 * 5、feign第一次调用慢的问题？
 *
 * </pre>
 */
@Configuration
public class FeignConfig {

    public static final String TRACE_ID = "TRACE_ID";

    /**
     * 解决feign同步调用请求头丢失的问题，如果不需要使用到当前线程的request,则此问题可以不用考虑。
     * 解决方法：定义一个拦截器{@link RequestInterceptor}，手动添加原请求的header信息
     */
    @Bean("allHeaderRequestInterceptor")
    RequestInterceptor allHeaderRequestInterceptor() {
        return requestTemplate -> {
            // 过滤器 org.springframework.web.filter.RequestContextFilter.doFilterInternal() 会创建 ServletRequestAttributes
            // 此处直接获取即可
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                Enumeration<String> headerNames = request.getHeaderNames();
                if (headerNames != null) {
                    while (headerNames.hasMoreElements()) {
                        String headerName = headerNames.nextElement();
                        String headerValue = request.getHeader(headerName);
                        if (!requestTemplate.headers().containsKey(headerName)) {
                            requestTemplate.header(headerName, headerValue);
                        }
                    }
                }
            }
        };
    }

    /**
     * 解决feign同步调用 TRACE_ID 请求头丢失的问题
     */
    @Bean("traceIdRequestInterceptor")
    RequestInterceptor traceIdRequestInterceptor() {
        return requestTemplate -> {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                // 从请求头中获取 TRACE_ID
                String traceId = request.getHeader(TRACE_ID);
                if (!StringUtils.hasText(traceId)) {
                    // 如果没有在请求头中获取 TRACE_ID，则去 request的Attribute 中取 TRACE_ID
                    Object attribute = request.getAttribute(TRACE_ID);
                    traceId = Objects.isNull(attribute) ? null : String.valueOf(attribute);
                }
                // 如果新请求中已经有 TRACE_ID 请求头，则不做任何处理
                if (StringUtils.hasText(traceId) && !requestTemplate.headers().containsKey(TRACE_ID)) {
                    // 设置 TRACE_ID 请求头
                    requestTemplate.header(TRACE_ID, traceId);
                }
            }
        };
    }

    /**
     * 解决feign异步调用中请求头丢失的问题
     * 原因：异步调用开启新的线程，新线程无法获取当前线程中的请求上下文，RequestContextHolder.getRequestAttributes()为 null
     * 解决：
     * 方案一：使用RequestContextHolder.setRequestAttributes()方法将父线程的Attributes设置到子线程；
     * 方案二：使用如下代码，让DispatcherServlet向子线程传递RequestContext
     */
    //@Bean
    public ServletRegistrationBean<DispatcherServlet> dispatcherRegistration(DispatcherServlet servlet) {
        servlet.setThreadContextInheritable(true);
        return new ServletRegistrationBean<>(servlet, "/**");
    }

}
